<?php
/**
 * TDataBoundControl class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2008 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id: TDataBoundControl.php 2492 2008-08-09 01:30:05Z knut $
 * @package System.Web.UI.WebControls
 */

Prado::using('System.Web.UI.WebControls.TDataSourceControl');
Prado::using('System.Web.UI.WebControls.TDataSourceView');
Prado::using('System.Collections.TPagedDataSource');

/**
 * TDataBoundControl class.
 *
 * TDataBoundControl is the based class for controls that need to populate
 * data from data sources. It provides basic properties and methods that allow
 * the derived controls to associate with data sources and retrieve data from them.
 *
 * TBC....
 *
 * TDataBoundControl is equipped with paging capabilities. By setting
 * {@link setAllowPaging AllowPaging} to true, the input data will be paged
 * and only one page of data is actually populated into the data-bound control.
 * This saves a lot of memory when dealing with larget datasets.
 *
 * To specify the number of data items displayed on each page, set
 * the {@link setPageSize PageSize} property, and to specify which
 * page of data to be displayed, set {@link setCurrentPageIndex CurrentPageIndex}.
 *
 * When the size of the original data is too big to be loaded all in the memory,
 * one can enable custom paging. In custom paging, the total number of data items
 * is specified manually via {@link setVirtualItemCount VirtualItemCount},
 * and the data source only needs to contain the current page of data. To enable
 * custom paging, set {@link setAllowCustomPaging AllowCustomPaging} to true.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: TDataBoundControl.php 2492 2008-08-09 01:30:05Z knut $
 * @package System.Web.UI.WebControls
 * @since 3.0
 */
abstract class TDataBoundControl extends TWebControl
{
	private $_initialized=false;
	private $_dataSource=null;
	private $_requiresBindToNull=false;
	private $_requiresDataBinding=false;
	private $_prerendered=false;
	private $_currentView=null;
	private $_currentDataSource=null;
	private $_currentViewValid=false;
	private $_currentDataSourceValid=false;
	private $_currentViewIsFromDataSourceID=false;
	private $_parameters=null;
	private $_isDataBound=false;

	/**
	 * @return Traversable data source object, defaults to null.
	 */
	public function getDataSource()
	{
		return $this->_dataSource;
	}

	/**
	 * Sets the data source object associated with the databound control.
	 * The data source must implement Traversable interface.
	 * If an array is given, it will be converted to xxx.
	 * If a string is given, it will be converted to xxx.
	 * @param Traversable|array|string data source object
	 */
	public function setDataSource($value)
	{
		$this->_dataSource=$this->validateDataSource($value);
		$this->onDataSourceChanged();
	}

	/**
	 * @return string ID path to the data source control. Defaults to empty.
	 */
	public function getDataSourceID()
	{
		return $this->getViewState('DataSourceID','');
	}

	/**
	 * @param string ID path to the data source control. The data source
	 * control must be locatable via {@link TControl::findControl} call.
	 */
	public function setDataSourceID($value)
	{
		$dsid=$this->getViewState('DataSourceID','');
		if($dsid!=='' && $value==='')
			$this->_requiresBindToNull=true;
		$this->setViewState('DataSourceID',$value,'');
		$this->onDataSourceChanged();
	}

	/**
	 * @return boolean if the databound control uses the data source specified
	 * by {@link setDataSourceID}, or it uses the data source object specified
	 * by {@link setDataSource}.
	 */
	protected function getUsingDataSourceID()
	{
		return $this->getDataSourceID()!=='';
	}

	/**
	 * Sets {@link setRequiresDataBinding RequiresDataBinding} as true if the control is initialized.
	 * This method is invoked when either {@link setDataSource} or {@link setDataSourceID} is changed.
	 */
	public function onDataSourceChanged()
	{
		$this->_currentViewValid=false;
		$this->_currentDataSourceValid=false;
		if($this->getInitialized())
			$this->setRequiresDataBinding(true);
	}

	/**
	 * @return boolean whether the databound control has been initialized.
	 * By default, the control is initialized after its viewstate has been restored.
	 */
	protected function getInitialized()
	{
		return $this->_initialized;
	}

	/**
	 * Sets a value indicating whether the databound control is initialized.
	 * If initialized, any modification to {@link setDataSource DataSource} or
	 * {@link setDataSourceID DataSourceID} will set {@link setRequiresDataBinding RequiresDataBinding}
	 * as true.
	 * @param boolean a value indicating whether the databound control is initialized.
	 */
	protected function setInitialized($value)
	{
		$this->_initialized=TPropertyValue::ensureBoolean($value);
	}

	/**
	 * @return boolean whether databind has been invoked in the previous page request
	 */
	protected function getIsDataBound()
	{
		return $this->_isDataBound;
	}

	/**
	 * @param boolean if databind has been invoked in this page request
	 */
	protected function setIsDataBound($value)
	{
		$this->_isDataBound=$value;
	}

	/**
	 * @return boolean whether a databind call is required (by the data bound control)
	 */
	protected function getRequiresDataBinding()
	{
		return $this->_requiresDataBinding;
	}

	/**
	 * @return boolean whether paging is enabled. Defaults to false.
	 */
	public function getAllowPaging()
	{
		return $this->getViewState('AllowPaging',false);
	}

	/**
	 * @param boolean whether paging is enabled
	 */
	public function setAllowPaging($value)
	{
		$this->setViewState('AllowPaging',TPropertyValue::ensureBoolean($value),false);
	}

	/**
	 * @return boolean whether the custom paging is enabled. Defaults to false.
	 */
	public function getAllowCustomPaging()
	{
		return $this->getViewState('AllowCustomPaging',false);
	}

	/**
	 * Sets a value indicating whether the custom paging should be enabled.
	 * When the pager is in custom paging mode, the {@link setVirtualItemCount VirtualItemCount}
	 * property is used to determine the paging, and the data items in the
	 * {@link setDataSource DataSource} are considered to be in the current page.
	 * @param boolean whether the custom paging is enabled
	 */
	public function setAllowCustomPaging($value)
	{
		$this->setViewState('AllowCustomPaging',TPropertyValue::ensureBoolean($value),false);
	}

	/**
	 * @return integer the zero-based index of the current page. Defaults to 0.
	 */
	public function getCurrentPageIndex()
	{
		return $this->getViewState('CurrentPageIndex',0);
	}

	/**
	 * @param integer the zero-based index of the current page
	 * @throws TInvalidDataValueException if the value is less than 0
	 */
	public function setCurrentPageIndex($value)
	{
		if(($value=TPropertyValue::ensureInteger($value))<0)
			$value=0;
		$this->setViewState('CurrentPageIndex',$value,0);
	}

	/**
	 * @return integer the number of data items on each page. Defaults to 10.
	 */
	public function getPageSize()
	{
		return $this->getViewState('PageSize',10);
	}

	/**
	 * @param integer the number of data items on each page.
	 * @throws TInvalidDataValueException if the value is less than 1
	 */
	public function setPageSize($value)
	{
		if(($value=TPropertyValue::ensureInteger($value))<1)
			throw new TInvalidDataValueException('databoundcontrol_pagesize_invalid',get_class($this));
		$this->setViewState('PageSize',TPropertyValue::ensureInteger($value),10);
	}

	/**
	 * @return integer number of pages of data items available
	 */
	public function getPageCount()
	{
		return $this->getViewState('PageCount',1);
	}

	/**
	 * @return integer virtual number of data items in the data source. Defaults to 0.
	 * @see setAllowCustomPaging
	 */
	public function getVirtualItemCount()
	{
		return $this->getViewState('VirtualItemCount',0);
	}

	/**
	 * @param integer virtual number of data items in the data source.
	 * @throws TInvalidDataValueException if the value is less than 0
	 * @see setAllowCustomPaging
	 */
	public function setVirtualItemCount($value)
	{
		if(($value=TPropertyValue::ensureInteger($value))<0)
			throw new TInvalidDataValueException('databoundcontrol_virtualitemcount_invalid',get_class($this));
		$this->setViewState('VirtualItemCount',$value,0);
	}

	/**
	 * Sets a value indicating whether a databind call is required by the data bound control.
	 * If true and the control has been prerendered while it uses the data source
	 * specified by {@link setDataSourceID}, a databind call will be called by this method.
	 * @param boolean whether a databind call is required.
	 */
	protected function setRequiresDataBinding($value)
	{
		$value=TPropertyValue::ensureBoolean($value);
		if($value && $this->_prerendered)
		{
			$this->_requiresDataBinding=true;
			$this->ensureDataBound();
		}
		else
			$this->_requiresDataBinding=$value;
	}

	/**
	 * Ensures any pending {@link dataBind} is called.
	 * This method calls {@link dataBind} if the data source is specified
	 * by {@link setDataSourceID} or if {@link getRequiresDataBinding RequiresDataBinding}
	 * is true.
	 */
	protected function ensureDataBound()
	{
		if($this->_requiresDataBinding && ($this->getUsingDataSourceID() || $this->_requiresBindToNull))
		{
			$this->dataBind();
			$this->_requiresBindToNull=false;
		}
	}

	/**
	 * @return TPagedDataSource creates a paged data source
	 */
	protected function createPagedDataSource()
	{
		$ds=new TPagedDataSource;
		$ds->setCurrentPageIndex($this->getCurrentPageIndex());
		$ds->setPageSize($this->getPageSize());
		$ds->setAllowPaging($this->getAllowPaging());
		$ds->setAllowCustomPaging($this->getAllowCustomPaging());
		$ds->setVirtualItemCount($this->getVirtualItemCount());
		return $ds;
	}

	/**
	 * Performs databinding.
	 * This method overrides the parent implementation by calling
	 * {@link performSelect} which fetches data from data source and does
	 * the actual binding work.
	 */
	public function dataBind()
	{
		$this->setRequiresDataBinding(false);
		$this->dataBindProperties();
		$this->onDataBinding(null);

		if(($view=$this->getDataSourceView())!==null)
			$data=$view->select($this->getSelectParameters());
		else
			$data=null;

		if($data instanceof Traversable)
		{
			if($this->getAllowPaging())
			{
				$ds=$this->createPagedDataSource();
				$ds->setDataSource($data);
				$this->setViewState('PageCount',$ds->getPageCount());
				if($ds->getCurrentPageIndex()>=$ds->getPageCount())
				{
					$ds->setCurrentPageIndex($ds->getPageCount()-1);
					$this->setCurrentPageIndex($ds->getCurrentPageIndex());
				}
				$this->performDataBinding($ds);
			}
			else
			{
				$this->clearViewState('PageCount');
				$this->performDataBinding($data);
			}
		}
		$this->setIsDataBound(true);
		$this->onDataBound(null);
	}

	public function dataSourceViewChanged($sender,$param)
	{
		if(!$this->_ignoreDataSourceViewChanged)
			$this->setRequiresDataBinding(true);
	}

	protected function getDataSourceView()
	{
		if(!$this->_currentViewValid)
		{
			if($this->_currentView && $this->_currentViewIsFromDataSourceID)
				$this->_currentView->detachEventHandler('DataSourceViewChanged',array($this,'dataSourceViewChanged'));
			if(($dataSource=$this->determineDataSource())!==null)
			{
				if(($view=$dataSource->getView($this->getDataMember()))===null)
					throw new TInvalidDataValueException('databoundcontrol_datamember_invalid',$this->getDataMember());
				if($this->_currentViewIsFromDataSourceID=$this->getUsingDataSourceID())
					$view->attachEventHandler('OnDataSourceViewChanged',array($this,'dataSourceViewChanged'));
				$this->_currentView=$view;
			}
			else
				$this->_currentView=null;
			$this->_currentViewValid=true;
		}
		return $this->_currentView;
	}

	protected function determineDataSource()
	{
		if(!$this->_currentDataSourceValid)
		{
			if(($dsid=$this->getDataSourceID())!=='')
			{
				if(($dataSource=$this->getNamingContainer()->findControl($dsid))===null)
					throw new TInvalidDataValueException('databoundcontrol_datasourceid_inexistent',$dsid);
				else if(!($dataSource instanceof IDataSource))
					throw new TInvalidDataValueException('databoundcontrol_datasourceid_invalid',$dsid);
				else
					$this->_currentDataSource=$dataSource;
			}
			else if(($dataSource=$this->getDataSource())!==null)
				$this->_currentDataSource=new TReadOnlyDataSource($dataSource,$this->getDataMember());
			else
				$this->_currentDataSource=null;
			$this->_currentDataSourceValid=true;
		}
		return $this->_currentDataSource;
	}

	abstract protected function performDataBinding($data);

	/**
	 * Raises <b>OnDataBound</b> event.
	 * This method should be invoked after a databind is performed.
	 * It is mainly used by framework and component developers.
	 */
	public function onDataBound($param)
	{
		$this->raiseEvent('OnDataBound',$this,$param);
	}

	/**
	 * Sets page's <b>OnPreLoad</b> event handler as {@link pagePreLoad}.
	 * If viewstate is disabled and the current request is a postback,
	 * {@link setRequiresDataBinding RequiresDataBinding} will be set true.
	 * This method overrides the parent implementation.
	 * @param TEventParameter event parameter
	 */
	public function onInit($param)
	{
		parent::onInit($param);
		$page=$this->getPage();
		$page->attachEventHandler('OnPreLoad',array($this,'pagePreLoad'));
	}

	/**
	 * Sets {@link getInitialized} as true.
	 * This method is invoked when page raises <b>PreLoad</b> event.
	 * @param mixed event sender
	 * @param TEventParameter event parameter
	 */
	public function pagePreLoad($sender,$param)
	{
		$this->_initialized=true;
		$isPostBack=$this->getPage()->getIsPostBack();
		if(!$isPostBack || ($isPostBack && (!$this->getEnableViewState(true) || !$this->getIsDataBound())))
			$this->setRequiresDataBinding(true);
	}

	/**
	 * Ensures any pending databind is performed.
	 * This method overrides the parent implementation.
	 * @param TEventParameter event parameter
	 */
	public function onPreRender($param)
	{
		$this->_prerendered=true;
		$this->ensureDataBound();
		parent::onPreRender($param);
	}

	/**
	 * Validates if the parameter is a valid data source.
	 * If it is a string or an array, it will be converted as a TList object.
	 * @param Traversable|array|string data source to be validated
	 * @return Traversable the data that is traversable
	 * @throws TInvalidDataTypeException if the data is neither null nor Traversable
	 */
	protected function validateDataSource($value)
	{
		if(is_string($value))
		{
			$list=new TList;
			foreach(TPropertyValue::ensureArray($value) as $key=>$value)
			{
				if(is_array($value))
					$list->add($value);
				else
					$list->add(array($value,is_string($key)?$key:$value));
			}
			return $list;
		}
		else if(is_array($value))
			return new TMap($value);
		else if($value instanceof TDbDataReader) {
			// read array from TDbDataReader since it's forward-only stream and can only be traversed once
			return $value->readAll();
		}
		else if(($value instanceof Traversable) || $value===null)
			return $value;
		else
			throw new TInvalidDataTypeException('databoundcontrol_datasource_invalid',get_class($this));
	}

	public function getDataMember()
	{
		return $this->getViewState('DataMember','');
	}

	public function setDataMember($value)
	{
		$this->setViewState('DataMember',$value,'');
	}

	public function getSelectParameters()
	{
		if(!$this->_parameters)
			$this->_parameters=new TDataSourceSelectParameters;
		return $this->_parameters;
	}
}


/**
 * TListItemType class.
 * TListItemType defines the enumerable type for the possible types
 * that databound list items could take.
 *
 * The following enumerable values are defined:
 * - Header: header item
 * - Footer: footer item
 * - Item: content item (neither header nor footer)
 * - Separator: separator between items
 * - AlternatingItem: alternating content item
 * - EditItem: content item in edit mode
 * - SelectedItem: selected content item
 * - Pager: pager
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: TDataBoundControl.php 2492 2008-08-09 01:30:05Z knut $
 * @package System.Web.UI.WebControls
 * @since 3.0.4
 */
class TListItemType extends TEnumerable
{
	const Header='Header';
	const Footer='Footer';
	const Item='Item';
	const Separator='Separator';
	const AlternatingItem='AlternatingItem';
	const EditItem='EditItem';
	const SelectedItem='SelectedItem';
	const Pager='Pager';
}


/**
 * IItemDataRenderer interface.
 *
 * IItemDataRenderer defines the interface that an item renderer
 * needs to implement. Besides the {@link getData Data} property, a list item
 * renderer also needs to provide {@link getItemIndex ItemIndex} and
 * {@link getItemType ItemType} property.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: TDataBoundControl.php 2492 2008-08-09 01:30:05Z knut $
 * @package System.Web.UI.WebControls
 * @since 3.1.0
 */
interface IItemDataRenderer extends IDataRenderer
{
	/**
	 * Returns a value indicating the zero-based index of the item in the corresponding data control's item collection.
	 * If the item is not in the collection (e.g. it is a header item), it returns -1.
	 * @return integer zero-based index of the item.
	 */
	public function getItemIndex();

	/**
	 * Sets the zero-based index for the item.
	 * If the item is not in the item collection (e.g. it is a header item), -1 should be used.
	 * @param integer zero-based index of the item.
	 */
	public function setItemIndex($value);

	/**
	 * @return TListItemType the item type.
	 */
	public function getItemType();

	/**
	 * @param TListItemType the item type.
	 */
	public function setItemType($value);
}

?>
